﻿#region Usings
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Kaleta.Xna.Distributed;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using Kaleta.Xna.Collision;
using Kaleta.Network;
using Kaleta.Xna.Extensions;
using Kaleta.Xna.Timing;
using Kaleta.Xna.Debug;
using Kaleta.Outcast.Network;
using Kaleta.Network.Serialization.Types;
#endregion

namespace Kaleta.Outcast
{
    public class Ship : DistributedSolidBody
    {
        #region Constructor
        public Ship()
        {
            this.Texture = Global.SpriteSheets.Ship.Texture;
            this.Body = new Body()
            {
                Origin = new Vector2(this.Texture.Width / 2f, this.Texture.Height / 2f),
                Scale = new Vector2(0.5f)
            };

            this.Position = new Vector2(100, 100);
            this.MovementDampingEnabled = true;
            this.MaxVelocity = 2.5f;

            #region Registration of distributed properties
            _syncData = DistributableProperty<ShipSyncData>.Register("SyncData", this, Lidgren.Network.NetChannel.ReliableInOrder1);
            _livetime = DistributableProperty<ObjectLivetime>.Register("Livetime", this, Lidgren.Network.NetChannel.ReliableInOrder1);
            _testString = DistributableProperty<DistributableString>.Register("TestString", this);
            #endregion

        }
        #endregion

        public override void Draw(SpriteBatch batch)
        {
            batch.Draw(this.Texture, this.Position, null, Color.White, this.Body.Direction, this.Body.Origin, this.Body.Scale, SpriteEffects.None, 0);
            Debugger.SetValue(string.Format("Ship[{0}] string", this.Key), TestString);
        }

        public override void Update(GameTime gameTime)
        {
            if (!this.IsCreatedLocally)
            {
                UpdateMovementGhost();
                ApplySmoothing(gameTime);
            }

            UpdateMovement();

            this.storyboard.Update(gameTime);
        }

        #region Private methods

        private void ApplySmoothing(GameTime gameTime)
        {
            this.Rotation = MathHelper.Lerp(this.Rotation, this.ghostRotation, this.smoothRange);
            this.Acceleration = MathHelper.Lerp(this.Acceleration, this.ghostAcceleration, this.smoothRange);
            this.Direction = MathHelper.Lerp(this.Direction, this.ghostDirection, this.smoothRange);

            this.Velocity = new Vector2()
            {
                X = MathHelper.Lerp(this.Velocity.X, this.ghostVelocity.X, this.smoothRange),
                Y = MathHelper.Lerp(this.Velocity.Y, this.ghostVelocity.Y, this.smoothRange)
            };
            this.Position = new Vector2()
            {
                X = MathHelper.Lerp(this.Position.X, this.ghostPosition.X, this.smoothRange),
                Y = MathHelper.Lerp(this.Position.Y, this.ghostPosition.Y, this.smoothRange)
            };
        }

        private void UpdateMovementGhost()
        {
            if (this.ghostMovementState.HasFlag(MovementState.Breaking))
            {
                this.ghostAcceleration = 0f;
                this.ghostVelocity = new Vector2()
                {
                    X = this.ghostVelocity.X - 0.05f * this.ghostVelocity.X,
                    Y = this.ghostVelocity.Y - 0.05f * this.ghostVelocity.Y
                };
            }
            else if (this.ghostMovementState.HasFlag(MovementState.Accelerating))
            {
                if (this.ghostAcceleration < 0.1f)
                    this.ghostAcceleration = 0.1f;
            }
            else
                this.ghostAcceleration = 0f;

            if (this.ghostMovementState.HasFlag(MovementState.RotatingRight))
                this.ghostRotation = 0.04f;
            else if (this.ghostMovementState.HasFlag(MovementState.RotatingLeft))
                this.ghostRotation = -0.04f;
            else
                this.ghostRotation = 0f;

            Vector2 value = this.ghostDirection.GetNormalizedVector2() * this.ghostAcceleration + this.ghostVelocity;
            if (this.MovementDampingEnabled) this.ghostVelocity = value.Clamp(this.MaxVelocity); else this.ghostVelocity = value;

            this.ghostPosition = new Vector2()
            {
                X = this.ghostPosition.X + this.ghostVelocity.X,
                Y = this.ghostPosition.Y + this.ghostVelocity.Y
            };

            this.ghostDirection += this.ghostRotation;



        }

        private void UpdateMovement()
        {
            if (this.MovementState.HasFlag(MovementState.Breaking))
            {
                this.Acceleration = 0f;
                this.Velocity = new Vector2()
                {
                    X = this.Velocity.X - 0.05f * this.Velocity.X,
                    Y = this.Velocity.Y - 0.05f * this.Velocity.Y
                };
            }
            else if (this.MovementState.HasFlag(MovementState.Accelerating))
            {
                if (this.Acceleration < 0.1f)
                    this.Acceleration = 0.1f;
            }
            else
                this.Acceleration = 0f;

            if (this.MovementState.HasFlag(MovementState.RotatingRight))
                this.Rotation = 0.04f;
            else if (this.MovementState.HasFlag(MovementState.RotatingLeft))
                this.Rotation = -0.04f;
            else
                this.Rotation = 0f;

            this.Velocity = this.Direction.GetNormalizedVector2() * this.Acceleration + this.Velocity;

            this.Body.Position = new Vector2()
            {
                X = this.Body.Position.X + this.Velocity.X,
                Y = this.Body.Position.Y + this.Velocity.Y
            };

            this.Body.Direction += this.Rotation;

        }

        /// <summary>
        /// Sends sync data all other clients to synchronize this local ship with all other clients.
        /// </summary>
        public void SendSyncData()
        {
            //save movement-state variables:
            Vector2 tempPosition = this.Position;
            Vector2 tempVelocity = this.Velocity;
            float tempRotation = this.Rotation;
            float tempAccel = this.Acceleration;
            float tempDirection = this.Direction;

            //lets calculate 'prediction' values of my sync data - update them according to my latency
            for (int i = 0; i < (int)(GetLatency() / Global.FrameUpdateTime); i++)
            {
                //update position times:
                this.UpdateMovement();
            }

            //send the 'prediction' (according to latency) updated sync data:
            SyncData = new ShipSyncData() { MovementState = this.MovementState, Position = this.Position, Rotation = this.Rotation, Direction = this.Direction, Acceleration = this.Acceleration, Velocity = this.Velocity };

            //set back movement-state variables to previous state:
            this.Position = tempPosition;
            this.Velocity = tempVelocity;
            this.Rotation = tempRotation;
            this.Acceleration = tempAccel;
            this.Direction = tempDirection;

        }
        #endregion

        #region User Commands

        public void Fire()
        {
            Bullet bullet = new Bullet();
            bullet.Position = this.Position;
            bullet.Direction = this.Direction;
            bullet.Velocity = this.Direction.GetNormalizedVector2() * 4.5f;
            bullet.GhostPosition = new Vector2()
            {
                X = bullet.Position.X + bullet.Velocity.X * DistributionSession.LatencyTester.GetLatency() / Global.FrameUpdateTime,
                Y = bullet.Position.Y + bullet.Velocity.Y * DistributionSession.LatencyTester.GetLatency() / Global.FrameUpdateTime
            };

            DistributionSession.DistributeObject(bullet);

            bullet.Livetime = ObjectLivetime.Created;

            TestString = new DistributableString("some string from obj" + this.Key.ToString());

        }

        public void Accelerate()
        {
            if (!this.MovementState.HasFlag(MovementState.Accelerating))
                this.MovementState |= MovementState.Accelerating;
        }

        public void Break()
        {
            if (!this.MovementState.HasFlag(MovementState.Breaking))
                this.MovementState |= MovementState.Breaking;
        }

        public void RotateLeft()
        {
            if (!this.MovementState.HasFlag(MovementState.RotatingLeft))
                this.MovementState |= MovementState.RotatingLeft;
        }

        public void RotateRight()
        {
            if (!this.MovementState.HasFlag(MovementState.RotatingRight))
                this.MovementState |= MovementState.RotatingRight;
        }

        #endregion

        #region Server-side events
        protected void OnMovementStateUpdate(ref MovementState oldValue)
        {
            var update = this.MovementState & ~oldValue;

            if (update.HasFlag(MovementState.Accelerating))
                this.MovementState &= ~MovementState.Breaking;

            if (update.HasFlag(MovementState.Breaking))
                this.MovementState &= ~MovementState.Accelerating;

            if (update.HasFlag(MovementState.RotatingLeft))
                this.MovementState &= ~MovementState.RotatingRight;

            if (update.HasFlag(MovementState.RotatingRight))
                this.MovementState &= ~MovementState.RotatingLeft;
        }

        public override void OnCollision(IBody obstacle, List<BodyShape> intersectingColliderShapes, List<BodyShape> intersectingObstacleShapes)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Invokes if sync data from server(host) has been recieved. Updates ships position according to sync data (Prediction algorithm).
        /// </summary>
        private void OnSynchronize()
        {
            this.smoothRange = 0f;

            //save movement-state variables:
            Vector2 tempPosition = this.Position;
            Vector2 tempVelocity = this.Velocity;
            float tempRotation = this.Rotation;
            float tempAccel = this.Acceleration;
            float tempDirection = this.Direction;
            MovementState tempState = this.MovementState;

            //from server:
            this.Position = SyncData.Position;
            this.Velocity = SyncData.Velocity;
            this.Rotation = SyncData.Rotation;
            this.Direction = SyncData.Direction;
            this.Acceleration = SyncData.Acceleration;
            this.MovementState = SyncData.MovementState;

            //then update movement variables according to my latency (only half of it, not the whole roundtrip latency):

            //prediction algorithm:
            for (int i = 0; i < (int)(GetLatency() / Global.FrameUpdateTime); i++)
                this.UpdateMovement();

            this.ghostAcceleration = this.Acceleration;
            this.ghostDirection = this.Direction;
            this.ghostPosition = this.Position;
            this.ghostRotation = this.Rotation;
            this.ghostVelocity = this.Velocity;
            this.ghostMovementState = this.MovementState;

            //set back movement-state variables to previous state:
            this.Position = tempPosition;
            this.Velocity = tempVelocity;
            this.Rotation = tempRotation;
            this.Acceleration = tempAccel;
            this.Direction = tempDirection;
            this.MovementState = tempState;

            this.Velocity = this.ghostVelocity;
            this.Rotation = this.ghostRotation;
            this.Acceleration = this.ghostAcceleration;
            this.MovementState = this.ghostMovementState;

            string state = Enum.GetName(typeof(MovementState), this.MovementState);
            string prevState = Debugger.GetValue<string>("Sync");
            if (state != null && state != prevState)
                Debugger.SetValue("Sync", state);


        }

        private void OnLivetimeUpdate()
        {
            if (this.Livetime == ObjectLivetime.Created)
            {
                if (!this.IsCreatedLocally)
                {
                    FloatTimeline syncSmoothing = new FloatTimeline(f => this.smoothRange += f, 0, 1f, 1500f, 0, false, false, -1);
                    this.storyboard.Add(syncSmoothing);
                }
                else
                {
                    ActionTimeline syncAction = new ActionTimeline(50f, SendSyncData, -1);
                    this.storyboard.Add(syncAction);
                }
            }

            if (this.Livetime == ObjectLivetime.Destroyed)
            {
                if (DistributionSession.Server == null)
                    return;

                DistributionSession.Dispose(this);
            }
        }


        private float GetLatency()
        {
            return DistributionSession.LatencyTester.GetLatency() / 2f;
        }

        #endregion

        #region Properties

        #region Public Properties

        public float Acceleration { get; protected set; }
        public float MaxVelocity { get; protected set; }
        public float Rotation { get; protected set; }



        private Vector2 _velocity;
        public Vector2 Velocity
        {
            get { return _velocity; }
            protected set { if (this.MovementDampingEnabled) _velocity = value.Clamp(this.MaxVelocity); else _velocity = value; }
        }

        public MovementState MovementState { get; set; }

        #endregion

        #region Distributed Properties

        [Distribute]
        public ShipSyncData SyncData
        {
            get
            {
                return _syncData.GetValue();
            }
            set
            {
                if (_syncData.SetValue(value))
                    if (!this.IsCreatedLocally)
                        OnSynchronize();
            }
        }
        private DistributableProperty<ShipSyncData> _syncData;


        [Distribute]
        public ObjectLivetime Livetime
        {
            get
            {
                return _livetime.GetValue();
            }
            set
            {
                if (_livetime.SetValue(value))
                    OnLivetimeUpdate();
            }
        }
        private DistributableProperty<ObjectLivetime> _livetime;


        [Distribute]
        public DistributableString TestString
        {
            get { return _testString.GetValue(); }
            set { _testString.SetValue(value); }
        }
        private DistributableProperty<DistributableString> _testString;

        #endregion

        #region Private Properties
        private bool MovementDampingEnabled { get; set; }
        private Storyboard storyboard = new Storyboard();
        private float smoothRange;

        public Vector2 ghostPosition;
        private Vector2 ghostVelocity;
        private float ghostRotation;
        private float ghostAcceleration;
        private float ghostDirection;
        private MovementState ghostMovementState;

        #endregion

        #endregion

    }

    #region Enums

    [Flags]
    public enum MovementState : uint
    {
        None = 0,
        Accelerating = 1,
        Breaking = 2,
        RotatingLeft = 4,
        RotatingRight = 8,
        BoostRotatingLeft = 16,
        BoostRotatingRight = 32,
        BoostAccelerating = 64,
        BoostReversingDirection = 128,
    }

    public static class EnumExtensions
    {
        public static bool HasFlag(this MovementState item, MovementState query)
        {
            return ((item & query) == query);
        }
    }

    #endregion

}
